﻿//
// Copyright 2011 Kazuki Oikawa
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Serialization;
#if NET20
using Pub.Class;
#else
#endif

namespace MsgPack.Compiler {
    static class PackILGenerator {
        #region Pack IL Generator
        public static void EmitPackCode(Type type, MethodInfo mi, ILGenerator il,
            Func<Type, MemberInfo[]> targetMemberSelector,
            Func<MemberInfo, string> memberNameFormatter,
            Func<Type, MethodInfo> lookupPackMethod) {
            if (type.IsPrimitive || type.IsInterface)
                throw new NotSupportedException();

            Variable arg_writer = Variable.CreateArg(0);
            Variable arg_obj = Variable.CreateArg(1);
            Variable local_i = Variable.CreateLocal(il.DeclareLocal(typeof(int)));

            if (!type.IsValueType) { // null check
                Label notNullLabel = il.DefineLabel();
                il.EmitLd(arg_obj);
                il.Emit(OpCodes.Brtrue_S, notNullLabel);
                il.EmitLd(arg_writer);
                il.Emit(OpCodes.Call, typeof(MsgPackWriter).GetMethod("WriteNil", new Type[0]));
                il.Emit(OpCodes.Ret);
                il.MarkLabel(notNullLabel);
            }

            if (type.IsArray) {
                EmitPackArrayCode(mi, il, type, arg_writer, arg_obj, local_i, lookupPackMethod);
                goto FinallyProcess;
            }

            // MsgPackWriter.WriteMapHeader
            MemberInfo[] members = targetMemberSelector(type);
            il.EmitLd(arg_writer);
            il.EmitLdc(members.Length);
            il.Emit(OpCodes.Callvirt, typeof(MsgPackWriter).GetMethod("WriteMapHeader", new Type[] { typeof(int) }));

            for (int i = 0; i < members.Length; i++) {
                MemberInfo m = members[i];
                Type mt = m.GetMemberType();

                // write field-name
                il.EmitLd(arg_writer);
                il.EmitLdstr(memberNameFormatter(m));
                il.EmitLd_True();
                il.Emit(OpCodes.Call, typeof(MsgPackWriter).GetMethod("Write", new Type[] { typeof(string), typeof(bool) }));

                // write value
                EmitPackMemberValueCode(mt, il, arg_writer, arg_obj, m, null, type, mi, lookupPackMethod);
            }

        FinallyProcess:
            il.Emit(OpCodes.Ret);
        }

        static void EmitPackArrayCode(MethodInfo mi, ILGenerator il, Type t, Variable var_writer, Variable var_obj, Variable var_loop, Func<Type, MethodInfo> lookupPackMethod) {
            Type et = t.GetElementType();
            il.EmitLd(var_writer, var_obj);
            il.Emit(OpCodes.Ldlen);
            il.Emit(OpCodes.Call, typeof(MsgPackWriter).GetMethod("WriteArrayHeader", new Type[] { typeof(int) }));

            Label beginLabel = il.DefineLabel();
            Label exprLabel = il.DefineLabel();

            // for-loop: init loop counter
            il.EmitLdc(0);
            il.EmitSt(var_loop);

            // jump
            il.Emit(OpCodes.Br_S, exprLabel);

            // mark begin-label
            il.MarkLabel(beginLabel);

            // write element
            EmitPackMemberValueCode(et, il, var_writer, var_obj, null, var_loop, t, mi, lookupPackMethod);

            // increment loop-counter
            il.EmitLd(var_loop);
            il.Emit(OpCodes.Ldc_I4_1);
            il.Emit(OpCodes.Add);
            il.EmitSt(var_loop);

            // mark expression label
            il.MarkLabel(exprLabel);

            // expression
            il.EmitLd(var_loop, var_obj);
            il.Emit(OpCodes.Ldlen);
            il.Emit(OpCodes.Blt_S, beginLabel);
        }

        /// <param name="m">(optional)</param>
        /// <param name="elementIdx">(optional)</param>
        static void EmitPackMemberValueCode(Type type, ILGenerator il, Variable var_writer, Variable var_obj,
            MemberInfo m, Variable elementIdx, Type currentType, MethodInfo currentMethod, Func<Type, MethodInfo> lookupPackMethod) {
            MethodInfo mi;
            il.EmitLd(var_writer, var_obj);
            if (m != null)
                il.EmitLdMember(m);
            if (elementIdx != null) {
                il.EmitLd(elementIdx);
                il.Emit(OpCodes.Ldelem, type);
            }
            if (type.IsPrimitive) {
                mi = typeof(MsgPackWriter).GetMethod("Write", new Type[] { type });
            } else {
                if (currentType == type) {
                    mi = currentMethod;
                } else {
                    mi = lookupPackMethod(type);
                }
            }
            il.Emit(OpCodes.Call, mi);
        }
        #endregion

        #region Unpack IL Generator
        public static void EmitUnpackCode(Type type, MethodInfo mi, ILGenerator il,
            Func<Type, MemberInfo[]> targetMemberSelector,
            Func<MemberInfo, string> memberNameFormatter,
            Func<Type, MethodInfo> lookupUnpackMethod,
            Func<Type, IDictionary<string, int>> lookupMemberMapping,
            MethodInfo lookupMemberMappingMethod) {
            if (type.IsArray) {
                EmitUnpackArrayCode(type, mi, il, targetMemberSelector, memberNameFormatter, lookupUnpackMethod);
            } else {
                EmitUnpackMapCode(type, mi, il, targetMemberSelector, memberNameFormatter, lookupUnpackMethod, lookupMemberMapping, lookupMemberMappingMethod);
            }
        }

        static void EmitUnpackMapCode(Type type, MethodInfo mi, ILGenerator il,
            Func<Type, MemberInfo[]> targetMemberSelector,
            Func<MemberInfo, string> memberNameFormatter,
            Func<Type, MethodInfo> lookupUnpackMethod,
            Func<Type, IDictionary<string, int>> lookupMemberMapping,
            MethodInfo lookupMemberMappingMethod) {
            MethodInfo failedMethod = typeof(PackILGenerator).GetMethod("UnpackFailed", BindingFlags.Static | BindingFlags.NonPublic);
            MemberInfo[] members = targetMemberSelector(type);
            IDictionary<string, int> member_mapping = lookupMemberMapping(type);
            for (int i = 0; i < members.Length; i++)
                member_mapping.Add(memberNameFormatter(members[i]), i);

            Variable msgpackReader = Variable.CreateArg(0);
            Variable obj = Variable.CreateLocal(il.DeclareLocal(type));
            Variable num_of_fields = Variable.CreateLocal(il.DeclareLocal(typeof(int)));
            Variable loop_idx = Variable.CreateLocal(il.DeclareLocal(typeof(int)));
            Variable mapping = Variable.CreateLocal(il.DeclareLocal(typeof(IDictionary<string, int>)));
            Variable switch_idx = Variable.CreateLocal(il.DeclareLocal(typeof(int)));
            Variable var_type = Variable.CreateLocal(il.DeclareLocal(typeof(Type)));

            // if (!MsgPackReader.Read()) UnpackFailed ();
            // if (MsgPackReader.Type == TypePrefixes.Nil) return null;
            // if (!MsgPackReader.IsMap ()) UnpackFailed ();
            EmitUnpackReadAndTypeCheckCode(il, msgpackReader, typeof(MsgPackReader).GetMethod("IsMap"), failedMethod, true);

            // type = typeof (T)
            il.Emit(OpCodes.Ldtoken, type);
            il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"));
            il.EmitSt(var_type);

            // mapping = LookupMemberMapping (typeof (T))
            il.EmitLd(var_type);
            il.Emit(OpCodes.Call, lookupMemberMappingMethod);
            il.EmitSt(mapping);

            // object o = FormatterServices.GetUninitializedObject (Type);
            il.EmitLd(var_type);
            il.Emit(OpCodes.Call, typeof(FormatterServices).GetMethod("GetUninitializedObject"));
            il.Emit(OpCodes.Castclass, type);
            il.EmitSt(obj);

            // num_of_fields = (int)reader.Length
            il.EmitLd(msgpackReader);
            il.Emit(OpCodes.Call, typeof(MsgPackReader).GetProperty("Length").GetGetMethod());
            il.EmitSt(num_of_fields);

            // Loop labels
            Label lblLoopStart = il.DefineLabel();
            Label lblLoopExpr = il.DefineLabel();

            // i = 0;
            il.EmitLdc(0);
            il.EmitSt(loop_idx);
            il.Emit(OpCodes.Br, lblLoopExpr);
            il.MarkLabel(lblLoopStart);

            /* process */
            // if (!MsgPackReader.Read() || !MsgPackReader.IsRaw()) UnpackFailed();
            EmitUnpackReadAndTypeCheckCode(il, msgpackReader, typeof(MsgPackReader).GetMethod("IsRaw"), failedMethod, false);

            // MsgPackReader.ReadRawString ()
            // if (!Dictionary.TryGetValue (,)) UnpackFailed();
            Label lbl3 = il.DefineLabel();
            il.EmitLd(mapping);
            il.EmitLd(msgpackReader);
            il.Emit(OpCodes.Call, typeof(MsgPackReader).GetMethod("ReadRawString", new Type[0]));
            il.Emit(OpCodes.Ldloca_S, (byte)switch_idx.Index);
            il.Emit(OpCodes.Callvirt, typeof(IDictionary<string, int>).GetMethod("TryGetValue"));
            il.Emit(OpCodes.Brtrue, lbl3);
            il.Emit(OpCodes.Call, failedMethod);
            il.MarkLabel(lbl3);

            // switch
            Label[] switchCases = new Label[members.Length];
            for (int i = 0; i < switchCases.Length; i++)
                switchCases[i] = il.DefineLabel();
            Label switchCaseEndLabel = il.DefineLabel();
            il.EmitLd(switch_idx);
            il.Emit(OpCodes.Switch, switchCases);
            il.Emit(OpCodes.Call, failedMethod);

            for (int i = 0; i < switchCases.Length; i++) {
                il.MarkLabel(switchCases[i]);
                MemberInfo minfo = members[i];
                Type mt = minfo.GetMemberType();
                MethodInfo unpack_method = lookupUnpackMethod(mt);
                il.EmitLd(obj);
                il.EmitLd(msgpackReader);
                il.Emit(OpCodes.Call, unpack_method);
                il.EmitStMember(minfo);
                il.Emit(OpCodes.Br, switchCaseEndLabel);
            }
            il.MarkLabel(switchCaseEndLabel);

            // i ++
            il.EmitLd(loop_idx);
            il.EmitLdc(1);
            il.Emit(OpCodes.Add);
            il.EmitSt(loop_idx);

            // i < num_of_fields;
            il.MarkLabel(lblLoopExpr);
            il.EmitLd(loop_idx);
            il.EmitLd(num_of_fields);
            il.Emit(OpCodes.Blt, lblLoopStart);

            // return
            il.EmitLd(obj);
            il.Emit(OpCodes.Ret);
        }

        static void EmitUnpackArrayCode(Type arrayType, MethodInfo mi, ILGenerator il,
            Func<Type, MemberInfo[]> targetMemberSelector,
            Func<MemberInfo, string> memberNameFormatter,
            Func<Type, MethodInfo> lookupUnpackMethod) {
            Type elementType = arrayType.GetElementType();
            MethodInfo failedMethod = typeof(PackILGenerator).GetMethod("UnpackFailed", BindingFlags.Static | BindingFlags.NonPublic);

            Variable msgpackReader = Variable.CreateArg(0);
            Variable obj = Variable.CreateLocal(il.DeclareLocal(arrayType));
            Variable num_of_elements = Variable.CreateLocal(il.DeclareLocal(typeof(int)));
            Variable loop_idx = Variable.CreateLocal(il.DeclareLocal(typeof(int)));
            Variable type = Variable.CreateLocal(il.DeclareLocal(typeof(Type)));

            // if (!MsgPackReader.Read() || !MsgPackReader.IsArray ()) UnpackFailed ();
            EmitUnpackReadAndTypeCheckCode(il, msgpackReader, typeof(MsgPackReader).GetMethod("IsArray"), failedMethod, true);

            // type = typeof (T)
            il.Emit(OpCodes.Ldtoken, elementType);
            il.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"));
            il.EmitSt(type);

            // num_of_elements = (int)reader.Length
            il.EmitLd(msgpackReader);
            il.Emit(OpCodes.Call, typeof(MsgPackReader).GetProperty("Length").GetGetMethod());
            il.EmitSt(num_of_elements);

            // object o = Array.CreateInstance (Type, Length);
            il.EmitLd(type);
            il.EmitLd(num_of_elements);
            il.Emit(OpCodes.Call, typeof(Array).GetMethod("CreateInstance", new Type[] { typeof(Type), typeof(int) }));
            il.Emit(OpCodes.Castclass, arrayType);
            il.EmitSt(obj);

            // Unpack element method
            MethodInfo unpack_method = lookupUnpackMethod(elementType);

            // Loop labels
            Label lblLoopStart = il.DefineLabel();
            Label lblLoopExpr = il.DefineLabel();

            // i = 0;
            il.EmitLdc(0);
            il.EmitSt(loop_idx);
            il.Emit(OpCodes.Br, lblLoopExpr);
            il.MarkLabel(lblLoopStart);

            /* process */
            il.EmitLd(obj, loop_idx);
            il.EmitLd(msgpackReader);
            il.Emit(OpCodes.Call, unpack_method);
            il.Emit(OpCodes.Stelem, elementType);

            // i ++
            il.EmitLd(loop_idx);
            il.EmitLdc(1);
            il.Emit(OpCodes.Add);
            il.EmitSt(loop_idx);

            // i < num_of_fields;
            il.MarkLabel(lblLoopExpr);
            il.EmitLd(loop_idx);
            il.EmitLd(num_of_elements);
            il.Emit(OpCodes.Blt, lblLoopStart);

            // return
            il.EmitLd(obj);
            il.Emit(OpCodes.Ret);
        }

        static void EmitUnpackReadAndTypeCheckCode(ILGenerator il, Variable msgpackReader, MethodInfo typeCheckMethod, MethodInfo failedMethod, bool nullCheckAndReturn) {
            Label lblFailed = il.DefineLabel();
            Label lblNullReturn = nullCheckAndReturn ? il.DefineLabel() : default(Label);
            Label lblPassed = il.DefineLabel();
            il.EmitLd(msgpackReader);
            il.Emit(OpCodes.Call, typeof(MsgPackReader).GetMethod("Read"));
            il.Emit(OpCodes.Brfalse_S, lblFailed);
            if (nullCheckAndReturn) {
                il.EmitLd(msgpackReader);
                il.Emit(OpCodes.Call, typeof(MsgPackReader).GetProperty("Type").GetGetMethod());
                il.EmitLdc((int)TypePrefixes.Nil);
                il.Emit(OpCodes.Beq_S, lblNullReturn);
            }
            il.EmitLd(msgpackReader);
            il.Emit(OpCodes.Call, typeCheckMethod);
            il.Emit(OpCodes.Brtrue_S, lblPassed);
            il.Emit(OpCodes.Br, lblFailed);
            if (nullCheckAndReturn) {
                il.MarkLabel(lblNullReturn);
                il.Emit(OpCodes.Ldnull);
                il.Emit(OpCodes.Ret);
            }
            il.MarkLabel(lblFailed);
            il.Emit(OpCodes.Call, failedMethod);
            il.MarkLabel(lblPassed);
        }

        /// <summary>Exception Helper</summary>
        internal static void UnpackFailed() {
            throw new FormatException();
        }
        #endregion

        #region Misc
        static Type GetMemberType(this MemberInfo mi) {
            if (mi.MemberType == MemberTypes.Field)
                return ((FieldInfo)mi).FieldType;
            if (mi.MemberType == MemberTypes.Property)
                return ((PropertyInfo)mi).PropertyType;
            throw new ArgumentException();
        }
        #endregion
    }
}
